/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package albumdownloader.ui.fx;

import albumdownloader.domain.entity.Album;
import albumdownloader.domain.entity.Page;
import albumdownloader.domain.entity.Song;
import albumdownloader.domain.service.MusicService;
import albumdownloader.domain.service.YiTingMusicServiceImpl;
import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.FadeTransition;
import javafx.animation.FadeTransitionBuilder;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.ScaleTransitionBuilder;
import javafx.animation.SequentialTransition;
import javafx.animation.SequentialTransitionBuilder;
import javafx.animation.Timeline;
import javafx.animation.TimelineBuilder;
import javafx.animation.TranslateTransitionBuilder;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.ImageViewBuilder;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.RotateBuilder;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;

/**
 *
 * @author max.m.yuan@gmail.com
 */
public class AlbumDownloaderController implements Initializable {

    private static final double SCALE_UP = 1.1;
    private static final double SCALE_DOWN = 1.0;
    private static final double DURATION_SHORT = 150.0;
    private static final double DURATION_LONG = 500.0;
    private static final double DURATION_ERROR = 3000;
    private static final String ERROR_PREFIX = "[ERROR]";
    private static final int COLUMNS = 7;
    private static final int ROWS = 4;
    private static final int NO_OF_ALBUMS = COLUMNS * ROWS;
    private double mouseDragOffsetX = 0;
    private double mouseDragOffsetY = 0;
    
    private SimpleBooleanProperty isBusy = null;
    private MusicService service = null;
    private SimpleStringProperty albumName = null;
    private SimpleStringProperty message = null;
    private SimpleIntegerProperty pageIndex = null;
    private Album currentAlbum = null;
    private SimpleStringProperty q = null;
    private SimpleIntegerProperty maxPage = null;
    private boolean doNotSearch = false;
    
    private ImageView currentCover = null;
    private ImageView[] imageViews = null;
    
    @FXML
    private GridPane albumGrid;
    @FXML
    private ImageView transitionCover;
    @FXML
    private VBox albumBox;
    @FXML
    private SearchBox searchBox;
    @FXML
    private Button previousBtn;
    @FXML
    private Button nextBtn;
    @FXML
    private StackPane blocker;
    @FXML
    private Rectangle shadow;
    @FXML
    private Button downloadBtn;
    @FXML
    private Button closeBtn;
    @FXML
    private Label albumNameLbl;
    @FXML
    private TableColumn songIdCol;
    @FXML
    private TableColumn songNameCol;
    @FXML
    private TableView<Song> songsTbl;
    @FXML
    private HBox messageBox;
    @FXML
    private Label messageLbl;
    
    private boolean searchingStarted = false;
    
    private Service<Void> searchingService = new Service<Void>() {
        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    updateMessage("Searching albums for \"" + q.get() + "\"...");
                    Page<Album> page = service.search(q.get(), NO_OF_ALBUMS, pageIndex.get());
                    if(page.getMaxPage() <= 0) {
                        throw new Exception("No albums found for \"" + q.get() + "\"");
                    }
                    maxPage.set(page.getMaxPage());
                    updateMessage("Loading album covers for \"" + q.get() + "\"...");
                    int size = page.getData().size();
                    imageViews = new ImageView[size];
                    for (int i = 0; i < size; i++) {
                        Album album = page.getData().get(i);
                        String url = album.getCoverUrl();
                        ImageView cover = ImageViewBuilder.create().fitHeight(108)
                                .fitWidth(108).pickOnBounds(true).preserveRatio(true)
                                .styleClass("album").image(new Image(url))
                                .userData(album).opacity(0)
                                .onMouseEntered(coverMouseEnteredHandler)
                                .onMouseExited(coverMouseExistedHandler)
                                .onMouseClicked(coverMouseClickedHandler).build();
                        imageViews[i] = cover;
                    }
                    updateMessage("");
                    return null;
                }
            };
        }
    };
    
    private void showNewAlbums() {
        for(int i = 0; i < imageViews.length; i++) {
            ImageView imageView = imageViews[i];
            int j = i >= COLUMNS ? i % COLUMNS : i;
            int k = (int) (i / COLUMNS);
            albumGrid.add(imageView, j, k);
        }
        
        SequentialTransition sequential = SequentialTransitionBuilder.create()
                .onFinished(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent t) {       
                isBusy.set(false);
            }
        }).build();
        for (ImageView imageView : imageViews) {
            FadeTransition fadeIn = FadeTransitionBuilder.create().
                    duration(Duration.millis(DURATION_SHORT)).node(imageView)
                    .fromValue(0).toValue(1).build();
            sequential.getChildren().add(fadeIn);
        }
        sequential.play();
    }

    private void hideOldAlbumsAndShowTheNewOnes() {
        SequentialTransition sequential = SequentialTransitionBuilder.create().onFinished(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent t) {
                albumGrid.getChildren().clear();
                showNewAlbums();
            }
        }).build();
        for (Node node : albumGrid.getChildren()) {
            if (node instanceof ImageView) {
                FadeTransition fadeOut = FadeTransitionBuilder.create().
                        duration(Duration.millis(DURATION_SHORT)).node(node)
                        .fromValue(1).toValue(0).build();
                sequential.getChildren().add(fadeOut);
            }
        }
        sequential.play();
    }
    
    private EventHandler<WorkerStateEvent> searchedHandler = new EventHandler<WorkerStateEvent>() {
        @Override
        public void handle(WorkerStateEvent t) {
            List<Node> oldAlbums = albumGrid.getChildren();
            if (oldAlbums.size() > 0) {
                hideOldAlbumsAndShowTheNewOnes();
            } else {
                showNewAlbums();
            }
        }
    };
    
    private boolean listingStarted = false;
    
    private Service<ObservableList<Song>> listingService = new Service<ObservableList<Song>>() {
        @Override
        protected Task<ObservableList<Song>> createTask() {
            return new Task<ObservableList<Song>>() {
                @Override
                protected ObservableList<Song> call() throws Exception {
                    updateMessage("Listing songs for album \"" + currentAlbum.getName() + "\"...");
                    List<Song> songs = service.listSongs(currentAlbum);
                    ObservableList<Song> list = FXCollections.observableArrayList();
                    list.setAll(songs);
                    updateMessage("");
                    return list;
                }
            };
        }
    };
    
    private static class CustomTableCell extends TableCell<Album, String> {
        @Override
        protected void updateItem(String t, boolean empty) {
            super.updateItem(t, empty);
            setText(empty ? null : getString());
            setGraphic(null);
        }

        private String getString() {
            return getItem() == null ? "" : getItem().toString();
        }
    }
    
    private Callback<TableColumn<Album, String>, TableCell<Album, String>> cellFactory = new Callback<TableColumn<Album, String>, TableCell<Album, String>>() {
        @Override
        public TableCell<Album, String> call(TableColumn<Album, String> p) {
            CustomTableCell cell = new CustomTableCell();
            cell.setAlignment(p == songIdCol ? Pos.CENTER : Pos.CENTER_LEFT);
            return cell;
        }
    };
    
    private boolean isBlank(String string) {
        return (string == null || string.trim().length() == 0);
    }
    
    private boolean isNotBlank(String string) {
        return (string != null && string.trim().length() > 0);
    }
    
    private EventHandler<ActionEvent> clearMessageHandler = new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            message.removeListener(messageChangedHandler);
            message.set("");
            messageLbl.setText("");
            message.addListener(messageChangedHandler);
        }
    };
    
    private ChangeListener<String> messageChangedHandler = new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> ov, String oldValue, String newValue) {
            double h = messageBox.getLayoutBounds().getHeight() + 3;
            boolean isError = isNotBlank(newValue) && newValue.startsWith(ERROR_PREFIX);
            ObservableList<String> styleClass = messageLbl.getStyleClass();
            if(isError) {
                newValue = newValue.replace(ERROR_PREFIX, "");
                styleClass.remove("info");
                styleClass.add("error");
                TranslateTransitionBuilder.create().node(messageBox)
                        .toY(-h).duration(Duration.millis(DURATION_SHORT))
                        .delay(Duration.millis(DURATION_ERROR))
                        .onFinished(clearMessageHandler).build().play();
            } else {
                styleClass.remove("error");
                styleClass.add("info");
            }
            if (isBlank(oldValue) && isNotBlank(newValue)) {
                messageLbl.setText(newValue);
                TranslateTransitionBuilder.create().node(messageBox)
                        .toY(h).duration(Duration.millis(DURATION_SHORT))
                        .build().play();
            } else if (isNotBlank(oldValue) && isBlank(newValue)) {
                TranslateTransitionBuilder.create().node(messageBox)
                        .toY(-h).duration(Duration.millis(DURATION_SHORT))
                        .onFinished(clearMessageHandler).build().play();
            } else {
                messageLbl.setText(newValue);
            }
        }
    };
    
    private EventHandler<WorkerStateEvent> serviceFailedHandler = new EventHandler<WorkerStateEvent>() {
        @Override
        public void handle(WorkerStateEvent t) {
            Throwable e = t.getSource().getException();
            message.set(ERROR_PREFIX + "Error: " + e.getMessage());
            Logger.getLogger(AlbumDownloaderController.class.getName())
                    .log(Level.SEVERE, "Failed to invoke service", e);
            isBusy.set(false);
        }
    };
    
    private ChangeListener<String> serviceMessageHandler = new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> ov, String t, String t1) {
            message.set(t1);
        }
    };
    
    private ChangeListener<Object> criteriaChanged = new ChangeListener<Object>() {
        @Override
        public void changed(ObservableValue<? extends Object> ov, Object t, Object t1) {
            if(doNotSearch || isBusy.get()) return;
            isBusy.set(true);

            if(searchingStarted) {
                searchingService.restart();
            } else {
                searchingService.start();
                searchingStarted = true;
            }
        }
    };
    
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        service = new YiTingMusicServiceImpl();
        isBusy = new SimpleBooleanProperty(false);
        albumName = new SimpleStringProperty();
        albumNameLbl.textProperty().bind(albumName);
        message = new SimpleStringProperty();
        message.addListener(messageChangedHandler);
        q = new SimpleStringProperty();
        q.addListener(criteriaChanged);
        pageIndex = new SimpleIntegerProperty(0);
        pageIndex.addListener(criteriaChanged);
        maxPage = new SimpleIntegerProperty(0);
        previousBtn.disableProperty().bind(pageIndex.lessThanOrEqualTo(0).or(isBusy));
        nextBtn.disableProperty().bind(pageIndex.greaterThanOrEqualTo(maxPage.add(-1)).or(isBusy));
        songIdCol.setCellValueFactory(new PropertyValueFactory("id"));
        songIdCol.setCellFactory(cellFactory);
        songNameCol.setCellValueFactory(new PropertyValueFactory("name"));
        songNameCol.setCellFactory(cellFactory);
        songsTbl.itemsProperty().bind(listingService.valueProperty());
        downloadBtn.disableProperty().bind(listingService.runningProperty().or(isBusy));
        closeBtn.disableProperty().bind(listingService.runningProperty().or(isBusy));
        searchingService.setOnSucceeded(searchedHandler);
        searchingService.setOnFailed(serviceFailedHandler);
        searchingService.messageProperty().addListener(serviceMessageHandler);
        listingService.setOnFailed(serviceFailedHandler);
        listingService.messageProperty().addListener(serviceMessageHandler);
        searchBox.focusOnMe();
    }
    
    public void bannerMousePressed(MouseEvent event) {
        mouseDragOffsetX = event.getSceneX();
        mouseDragOffsetY = event.getSceneY();
    }

    public void bannerMouseDragged(MouseEvent event) {
        Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow();
        stage.setX(event.getScreenX() - mouseDragOffsetX);
        stage.setY(event.getScreenY() - mouseDragOffsetY);
    }

    public void minimize(ActionEvent event) {
        Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow();
        stage.setIconified(true);
    }

    public void close(ActionEvent event) {
        Platform.exit();
    }
    
    public void search(KeyEvent event) {
        TextField textBox = (TextField) event.getSource();
        String text = textBox.getText().trim();
        if (event.getCode() == KeyCode.ENTER && !text.isEmpty()) {
            doNotSearch = true;
            pageIndex.set(0);
            doNotSearch = false;
            q.set(text);
        }
    }
    
    public void previous(ActionEvent event) {
        int _pageIndex = pageIndex.get();
        if(_pageIndex > 0) {
            pageIndex.set(_pageIndex - 1);
        }
    }
    
    public void next(ActionEvent event) {
        int _pageIndex = pageIndex.get();
        if(_pageIndex < (maxPage.get() - 1)) {
            pageIndex.set(_pageIndex + 1);
        }
    }
    
    private EventHandler<MouseEvent> coverMouseEnteredHandler = new EventHandler<MouseEvent>() {
        @Override
        public void handle(MouseEvent t) {
            Node node = (Node) t.getSource();
            ScaleTransitionBuilder.create().duration(Duration.millis(DURATION_SHORT))
                    .node(node).fromX(SCALE_DOWN).toX(SCALE_UP)
                    .fromY(SCALE_DOWN).toY(SCALE_UP).build().play();
        }
    };
    
    private EventHandler<MouseEvent> coverMouseExistedHandler = new EventHandler<MouseEvent>() {
        @Override
        public void handle(MouseEvent t) {
            Node node = (Node) t.getSource();
            ScaleTransitionBuilder.create().duration(Duration.millis(DURATION_SHORT))
                    .node(node).fromX(SCALE_UP).toX(SCALE_DOWN)
                    .fromY(SCALE_UP).toY(SCALE_DOWN).build().play();
        }
    };
    
    private EventHandler<MouseEvent> coverMouseClickedHandler = new EventHandler<MouseEvent>() {
        @Override
        public void handle(MouseEvent t) {
            if(isBusy.get()) return;
            isBusy.set(true);
            
            currentCover = (ImageView) t.getSource();
            currentAlbum = (Album) currentCover.getUserData();
            Image image = new Image(currentAlbum.getBigCoverUrl());
            
            albumName.set(currentAlbum.getName());
            if(listingStarted) {
                listingService.restart();
            } else {
                listingService.start();
                listingStarted = true;
            }
            
            double x = currentCover.getLayoutX(), y = currentCover.getLayoutY();
            double tx = currentCover.getTranslateX(), ty = currentCover.getTranslateY();
            double w = currentCover.getLayoutBounds().getWidth(), h = currentCover.getLayoutBounds().getHeight();
            double cx = currentCover.getScaleX(), cy = currentCover.getScaleY();
            Point2D layoutXY = albumGrid.localToParent(x, y);
            double _x = layoutXY.getX() + tx - w * (cx - 1) / 2;
            double _y = layoutXY.getY() + ty - h * (cy - 1) / 2;
            transitionCover.setLayoutX(_x);
            transitionCover.setLayoutY(_y);
            transitionCover.setFitWidth(w * cx);
            transitionCover.setFitHeight(h * cy);
            transitionCover.setImage(image);
            transitionCover.setVisible(true);
            currentCover.setVisible(false);
            blocker.setVisible(true);
            Timeline moveOutAndEnlargeAnimation = TimelineBuilder.create().keyFrames(
                    new KeyFrame(
                        Duration.millis(DURATION_LONG),
                        new KeyValue(transitionCover.layoutXProperty(), 512),
                        new KeyValue(transitionCover.layoutYProperty(), 127),
                        new KeyValue(transitionCover.fitWidthProperty(), 400),
                        new KeyValue(transitionCover.fitHeightProperty(), 400)
                    )
                ).onFinished(movedOutAndEnlargedHandler).build();
            moveOutAndEnlargeAnimation.play();
        }
    };
    
    private EventHandler<ActionEvent> unfoldedHandler = new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent t) {
            shadow.setVisible(true);
            downloadBtn.requestFocus();
            isBusy.set(false);
        }
    };
    
    public void closeAlbumBox(MouseEvent event) {
        if(isBusy.get()) return;
        isBusy.set(true);
        
        shadow.setVisible(false);
        Rotate rotation = RotateBuilder.create().axis(Rotate.Y_AXIS).build();
        transitionCover.getTransforms().add(rotation);
        Timeline foldAnimation = TimelineBuilder.create().keyFrames(
                new KeyFrame(
                    Duration.millis(DURATION_LONG),
                    new KeyValue(rotation.angleProperty(), -180)
                )
            ).onFinished(foldedHandler).build();
        foldAnimation.play();
    }
    
    private EventHandler<ActionEvent> movedOutAndEnlargedHandler = new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent t) {
            albumBox.setVisible(true);
            Rotate rotation = RotateBuilder.create().axis(Rotate.Y_AXIS).build();
            transitionCover.getTransforms().add(rotation);
            Timeline unfoldAnimation = TimelineBuilder.create().keyFrames(
                    new KeyFrame(
                        Duration.millis(DURATION_LONG),
                        new KeyValue(rotation.angleProperty(), 180)
                    )
                ).onFinished(unfoldedHandler).build();
            unfoldAnimation.play();
        }
    };
    
    private EventHandler<ActionEvent> shrinkedAndMovedBackHandler = new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent t) {
            currentCover.setVisible(true);
            transitionCover.setVisible(false);
            blocker.setVisible(false);
            isBusy.set(false);
        }
    };
    
    private EventHandler<ActionEvent> foldedHandler = new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent t) {
            albumBox.setVisible(false);
            double x = currentCover.getLayoutX(), y = currentCover.getLayoutY();
            double tx = currentCover.getTranslateX(), ty = currentCover.getTranslateY();
            double w = currentCover.getLayoutBounds().getWidth(), h = currentCover.getLayoutBounds().getHeight();
            double cx = currentCover.getScaleX(), cy = currentCover.getScaleY();
            Point2D layoutXY = albumGrid.localToParent(x, y);
            double _x = layoutXY.getX() + tx;
            double _y = layoutXY.getY() + ty;
            double _w = w * cx;
            double _h = h * cy;
            Timeline shrinkAndMoveBackAnimation = TimelineBuilder.create().keyFrames(
                    new KeyFrame(
                        Duration.millis(DURATION_LONG),
                        new KeyValue(transitionCover.layoutXProperty(), _x),
                        new KeyValue(transitionCover.layoutYProperty(), _y),
                        new KeyValue(transitionCover.fitWidthProperty(), _w),
                        new KeyValue(transitionCover.fitHeightProperty(), _h)
                    )
                ).onFinished(shrinkedAndMovedBackHandler).build();
            shrinkAndMoveBackAnimation.play();
        }
    };
}
